﻿/************************************************************************

   Extended Silverlight Toolkit

   Copyright (C) 2010-2012 Xceed Software Inc.

   This program is provided to you under the terms of the Microsoft Public
   License (Ms-PL) as published at http://extendedsilverlight.codeplex.com/license

   Please purchase a commercial version of this toolkit if you are using
   it in a commercial product. The commercial versions support the project,
   and also include more features for each control, priority support,
   source code, updates, removed watermark, and a proprietary license.

   Visit http://xceed.com and follow @datagrid on Twitter.

  **********************************************************************/

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.Windows;
using Xceed.Silverlight.Data;

namespace Xceed.Silverlight.DataGrid
{
  public partial class DataGridControl
  {
    private void ApplySelectionUpdate( SelectionUpdate update )
    {
      if( update == null )
        return;

      ObservableCollection<SelectionRange>  selectionRangeCollection = ( ObservableCollection<SelectionRange> )this.DataGridContext.SelectedRanges;
      selectionRangeCollection.CollectionChanged -= new NotifyCollectionChangedEventHandler( this.OnSelectedRangesChanged );

      DataPath currentPath = this.DataGridContext.GetActualDataPathOrDataPath( this.CurrentPath );

      if( currentPath == null )
        throw new DataGridInternalException();

      if( update.UpdateAnchor || m_anchorPath == null )
      {
        m_anchorPath = (!currentPath.IsGroupPath()) ? currentPath : null;
      }

      if( update.ClearSelection )
      {
        this.DataGridContext.SelectedRanges.Clear();
        this.ClearSelectedRanges();
      }

      if( m_anchorPath != null && !currentPath.IsGroupPath() )
      {
        bool anchorSelectionState = this.IsPathSelected( m_anchorPath );

        if( update.ReverseAnchorSelectionState )
        {
          anchorSelectionState = !anchorSelectionState;
        }

        SelectionRange selectionRange = new SelectionRange();
        selectionRange.SelectionType = ( anchorSelectionState ) ? SelectionType.Selection : SelectionType.Unselection;

        this.AssignPathsToRangeDictionaries(
          m_anchorPath,
          currentPath,
          this.DataGridContext.DataSource.AppliedSortDescriptions,
          selectionRange );

        selectionRange.StartRangeInfos.IsInclusive = true;
        selectionRange.EndRangeInfos.IsInclusive = true;

        this.DataGridContext.SelectedRanges.Add( selectionRange );
        List<SelectionRange> selectionRangeList = new List<SelectionRange>();
        selectionRangeList.Add( selectionRange );
        this.HandleSelectionRangeAddition( selectionRangeList );
      }

      this.UpdateSelectedRanges();

      selectionRangeCollection.CollectionChanged += new NotifyCollectionChangedEventHandler( this.OnSelectedRangesChanged );

    }

    internal void ClearAllSelections()
    {
      this.DataGridContext.SelectedRanges.Clear();
      this.DataGridContext.SelectedItem = null; 
    }

    private void SelectAllHelper()
    {
      this.DataGridContext.SelectedRanges.Add( SelectionRange.All );
    }

    private SelectionRange FindSelectionRangeForPath( DataGridPath path )
    {
      // Stub path never belonged to a path.
      if( path.ActualDataPath.IsStubPath() )
        return null;

      SelectionRange retVal = null;

      // Start from end to have the latest row status (selected, unselected)
      for( int i = this.DataGridContext.SelectedRanges.Count - 1; i >= 0; --i )
      {
        SelectionRange selectionRange = this.DataGridContext.SelectedRanges[ i ];

        //If the start matches
        if( IsPathRespectLowerBound( selectionRange, path ) )
        {
          //If the end also matches
          if( IsPathRespectHigherBound( selectionRange, path ) )
          {
            //check for any filter
            object userObject = this.DataGridContext.DataSource.DataHelper.ConvertBack( path.ActualDataPath );
              if( userObject != null && !selectionRange.IsFilteredOut( userObject ) )
              {
                //We have a matching SelectionRange
                retVal = selectionRange;

              //Exit loop right away, no need to check further (we can only match one range).
              break;
            }
          }
        }
      }

      return retVal;
    }

    private bool IsPathRespectLowerBound( SelectionRange currentRange, DataGridPath referencePath )
    {
      bool isLowerBoundInclusive;
      IDictionary<String, object> rangeLowerBound = currentRange.GetLowerBound( out isLowerBoundInclusive );

      bool isAllBoundSortsContainedInOtherBoundSorts = currentRange.IsAllBoundSortsContainedInOtherBoundSorts( rangeLowerBound );

      int startCompare = this.CompareSelectionRangeBoundWithPath( currentRange.SortDescriptions, rangeLowerBound, true, isLowerBoundInclusive,
                                                                  isAllBoundSortsContainedInOtherBoundSorts, referencePath );
      if( ( startCompare > 0 )
        || ( isLowerBoundInclusive && ( startCompare == 0 ) ) )
      {
        return true;
      }

      return false;
    }

    private bool IsPathRespectHigherBound( SelectionRange currentRange, DataGridPath referencePath )
    {
      bool isHigherBoundInclusive;
      IDictionary<String, object> rangeHigherBound = currentRange.GetHigherBound( out isHigherBoundInclusive );

      bool isAllBoundSortsContainedInOtherBoundSorts = currentRange.IsAllBoundSortsContainedInOtherBoundSorts( rangeHigherBound );

      int endCompare = this.CompareSelectionRangeBoundWithPath( currentRange.SortDescriptions, rangeHigherBound, false, isHigherBoundInclusive,
                                                                isAllBoundSortsContainedInOtherBoundSorts, referencePath );
      if( ( endCompare < 0 )
          || ( isHigherBoundInclusive && ( endCompare == 0 ) ) )
      {
        return true;
      }

      return false;
    }

    private int CompareSelectionRangeBoundWithPath( ReadOnlyCollection<SortDescription> sortDescriptions, IDictionary<String, object> rangeBound,
                                                    bool isLowerBound, bool isRangeBoundInclusive, bool isAllBoundSortsContainedInOtherBoundSorts, 
                                                    DataGridPath referencePath )
    {
      return this.CompareSelectionRangeHelper( sortDescriptions,
                                               rangeBound,
                                               referencePath.ActualDataPath.LastChild,
                                               isLowerBound,
                                               isRangeBoundInclusive,
                                               isAllBoundSortsContainedInOtherBoundSorts );
    }

    private int CompareSelectionRangeHelper( IList<SortDescription> sortDescriptions, IDictionary<String, object> keyValues, DataItem referenceDataGridItem,
                                             bool isLowerBound, bool isRangeBoundInclusive, bool isAllBoundSortsContainedInOtherBoundSorts )
    {
      if( sortDescriptions == null )
        throw new ArgumentNullException( "sortDescriptions" );

      if( keyValues == null )
        throw new ArgumentNullException( "keyValues" );

      if( referenceDataGridItem == null )
        throw new ArgumentNullException( "referenceDataGridItem" );

      if( referenceDataGridItem.Data == null )
        throw new ArgumentException( "The specified DataItem does not contain data.", "referenceDataGridItem" );

      foreach( SortDescription sortDesc in sortDescriptions )
      {
        string propName = sortDesc.PropertyName;

        object keyValue;

        if( !keyValues.TryGetValue( propName, out keyValue ) )
        {

          if( isLowerBound )
          {
            if( isAllBoundSortsContainedInOtherBoundSorts )
              return 1;

            return isRangeBoundInclusive ? 1 : -1;
          }
          else
          {
            if( isAllBoundSortsContainedInOtherBoundSorts )
              return -1;
            return isRangeBoundInclusive ? -1 : 1;
          }
        }

        object referenceValue = this.DataGridContext.DataSource.GetDataObjectPropertyValue( propName, referenceDataGridItem );

        int result = this.CompareHelper( referenceValue, keyValue );

        if( result < 0 )
        {
          if( sortDesc.Direction == ListSortDirection.Ascending )
          {
            return -1;
          }
          else
          {
            return 1;
          }
        }

        if( result > 0 )
        {
          if( sortDesc.Direction == ListSortDirection.Ascending )
          {
            return 1;
          }
          else
          {
            return -1;
          }
        }
      }

      return 0;
    }

    private int CompareHelper( object a, object b )
    {
      if( a == b )
        return 0;

      if( a == null )
        return -1;

      if( b == null )
        return 1;

      IComparable comparable = a as IComparable;

      if( comparable == null )
        throw new InvalidOperationException( "An attempt was made to compare objects that do not implement IComparable." );

      return comparable.CompareTo( b );
    }

    private void UpdateSelection()
    {
      DataRowVisitor visitor = new DataRowVisitor(ChildrenPolicy.RenderedChildrenOnly, ( row ) => this.UpdateSelectionForElement( row ) );
      this.RootContainer.Accept( visitor );
    }

    private void UpdateSelectionForElement( DataRow dataRow )
    {
      this.SetElementSelection( dataRow, this.IsPathSelected( dataRow.Path ) );
    }

    private bool IsPathSelected( DataPath path )
    {
      SelectionRange selectedRange = this.FindSelectionRangeForPath( new DataGridPath( path ) );
      return ( selectedRange != null ) && ( selectedRange.SelectionType == SelectionType.Selection );
    }

    private void UnselectAll()
    {
      this.DataGridContext.SelectedRanges.Clear();
    }

    private void ExternalSetSelectedItem( DependencyPropertyChangedEventArgs e )
    {
      if( e.NewValue != null )
      {
        SelectionRange rangeToAdd = new SelectionRange( e.NewValue );
        rangeToAdd.SelectionType = SelectionType.Selection;
        this.DataGridContext.SelectedRanges.Add( rangeToAdd );
      }
      else
      {
        this.DataGridContext.SelectedRanges.Clear();
      }
    }

    private void OnSelectedRangesChanged( object sender, NotifyCollectionChangedEventArgs e )
    {
      if( this.DataGridContext.DataSource != null )
      {
        switch( e.Action )
        {
          case NotifyCollectionChangedAction.Add:
          case NotifyCollectionChangedAction.Replace:
            this.HandleSelectionRangeAddition( e.NewItems );
            break;

          case NotifyCollectionChangedAction.Remove:
          case NotifyCollectionChangedAction.Reset:
          default:
            this.ClearSelectedRanges();
            break;
        }

        this.UpdateSelectedRanges();
      }
    }

    private void ClearSelectedRanges()
    {
      this.SetSelectedItemInternal( null );  
    }

    private void HandleSelectionRangeAddition( IList addedSelectionRanges )
    {
      DataSourceWrapper dataSource = this.DataGridContext.DataSource;

      Debug.Assert( dataSource != null );

      if( dataSource == null )
        return;

      foreach( SelectionRange selectionRange in addedSelectionRanges )
      {
        if( selectionRange.UseDefaultDataRangeContext && !selectionRange.IsSingleItem )
        {
          selectionRange.DataRangeContext = dataSource.GetCurrentDataRangeContext();
        }
        else
        {
          selectionRange.DataRangeContext = Xceed.Silverlight.DataGrid.SelectionRangeHelper.BuildDataRangeContext( dataSource, selectionRange );
        }

        ValidateSelectionRange( selectionRange );

        if( ( selectionRange.StartRangeInfos.GetFromItem() != null ) || ( selectionRange.EndRangeInfos.GetToItem() != null ) )
        {
          ConvertRangeInfoItemsToKeyValues( selectionRange );
        }

        //for sync sources only
        if( !this.IsDataSourceAsynchronous )
        {
          if( selectionRange.SelectionType == SelectionType.Selection )
          {
            if( ( this.CurrentDataPath != null ) && this.IsPathSelected( this.CurrentDataPath ) )
            {
              this.SetSelectedItemInternal( this.CurrentDataPath.LastChild.Data );
            }
            else
            {
              this.SetSelectedItemInternal( null );
            }
          }
          else
          {
            if( this.DataGridContext.SelectedItem != null )
            {
              SelectionRange SelectedItemRange = this.FindSelectionRangeForPath( this.GetPathFromItem( this.DataGridContext.SelectedItem ) );
              if( object.Equals( SelectedItemRange, selectionRange ) )
              {
                this.SetSelectedItemInternal( null );
              }
            }
          }
        }
      }
    }

    private void UpdateSelectedRanges()
    {
      //Raise Internal event to resynchronize all SelectedRanges on layout elements.
      this.UpdateSelection();

      //Raise the event to notify user of changes in the Selection.
      this.RaiseSelectionChanged();
    }

    private void ValidateSelectionRange( SelectionRange selectionRange )
    {
      if( selectionRange.IsEmpty )
        throw new ArgumentException( "The specified selection range cannot be empty.", "selectionRange" );

      if( !selectionRange.HaveEndLimit )
        throw new ArgumentException( "The specified selection range must provide an end limit.", "selectionRange" );

      if( !selectionRange.HaveStartLimit )
        throw new ArgumentException( "The specified selection range must provide a start limit.", "selectionRange" );

      if( this.DataGridContext.DataSource == null )
        throw new InvalidOperationException( "An attempt was made to add a selection range while the grid is not bound to a data source." );

      if( this.DataGridContext.DataSource.IsPrimaryKeyVolatile
        && ( selectionRange.StartRangeInfos.GetFromItem() != null
        || selectionRange.EndRangeInfos.GetToItem() != null ) )
      {
        throw new InvalidOperationException( "An attempt was made to add a selection range that defines an item as the start or end range infos to a data source whose primary keys may have changed." );
      }

      if( selectionRange.SortDescriptions.Count > 0 )
      {
        bool canEvaluateLowerBound = true;
        bool canEvaluateHigherBound = true;

        bool isLowerBoundInclusive;
        IDictionary<String, object> lowerBound = selectionRange.GetLowerBound( out isLowerBoundInclusive );
        bool isHigherBoundInclusive;
        IDictionary<String, object> higherBound = selectionRange.GetHigherBound( out isHigherBoundInclusive );

        if( selectionRange.StartRangeInfos.GetFromItem() != null )
        {
          if( lowerBound == ( IDictionary<String, object> )selectionRange.StartRangeInfos )
            canEvaluateLowerBound = false;
          else if( higherBound == ( IDictionary<String, object> )selectionRange.StartRangeInfos )
            canEvaluateHigherBound = false;
        }
        if( selectionRange.EndRangeInfos.GetToItem() != null )
        {
          if( lowerBound == ( IDictionary<String, object> )selectionRange.EndRangeInfos )
            canEvaluateLowerBound = false;
          else if( higherBound == ( IDictionary<String, object> )selectionRange.EndRangeInfos )
            canEvaluateHigherBound = false;
        }

        if( canEvaluateLowerBound && selectionRange.IsAllBoundSortsContainedInOtherBoundSorts( lowerBound ) && !isLowerBoundInclusive )
          throw new InvalidOperationException( "The range's lower bound is exclusive and won't be considered if the sort criteria of a bound are also present in the another bound." );

        else if( canEvaluateHigherBound && selectionRange.IsAllBoundSortsContainedInOtherBoundSorts( higherBound ) && !isHigherBoundInclusive )
          throw new InvalidOperationException( "The range's upper bound is exclusive and won't be considered if the sort criteria of a bound are also present in the another bound." );
      }
    }

    private void ConvertRangeInfoItemsToKeyValues( SelectionRange selectionRange )
    {
      DataSourceWrapper dataSource = this.DataGridContext.DataSource;

      Debug.Assert( dataSource != null );

      if( dataSource == null )
        return;
      if( selectionRange == null )
        return;

      DataPath startDatapath = null;
      DataPath endDatapath = null;
      if( selectionRange.StartRangeInfos.GetFromItem() != null )
      {
        startDatapath = dataSource.DataHelper.Convert( selectionRange.StartRangeInfos.GetFromItem() );
      }
      if( selectionRange.EndRangeInfos.GetToItem() != null )
      {
        endDatapath = dataSource.DataHelper.Convert( selectionRange.EndRangeInfos.GetToItem() );
      }

      if( ( startDatapath == null ) && ( endDatapath == null ) )
      {
        throw new InvalidOperationException( "Unable to retrieve the data path from the specified selection range information." );
      }

      if( ( startDatapath != null ) && ( endDatapath != null )
       && dataSource.DataHelper.Compare( selectionRange.SortDescriptions, startDatapath, endDatapath ) > 0 )
      {
        //This means the start is "bigger" than the end. To have consistent Data, it is better that the Start is always before the End.
        object swap = selectionRange.EndRangeInfos.GetToItem();
        bool isSwapInclusive = selectionRange.EndRangeInfos.IsInclusive;
        selectionRange.EndRangeInfos.ToItem( selectionRange.StartRangeInfos.GetFromItem() );
        selectionRange.EndRangeInfos.IsInclusive = selectionRange.StartRangeInfos.IsInclusive;
        selectionRange.StartRangeInfos.FromItem( swap );
        selectionRange.StartRangeInfos.IsInclusive = isSwapInclusive;

        DataPath dataPathSwap = endDatapath;
        endDatapath = startDatapath;
        startDatapath = dataPathSwap;
      }

      this.AssignPathsToRangeDictionaries( startDatapath, endDatapath, selectionRange.SortDescriptions, selectionRange );
    }

    private void SetSelectedItemInternal( object item )
    {
      if( !this.IsDataSourceAsynchronous )
      {
        m_internalSetSelectedItem = true;

        if( item == null )
        {
          this.DataGridContext.SelectedItem = ( this.DataGridContext.SelectedItems.Count > 0 ) ? this.DataGridContext.SelectedItems[ 0 ] : null;
        }
        else
        {
          this.DataGridContext.SelectedItem = item;
        }

        m_internalSetSelectedItem = false;
      }
    }

    private void SetElementSelection( DataRow dataRow, bool selection )
    {
      dataRow.IsSelected = selection;
      this.UpdateRowCommonState( dataRow );
    }

    private void RaiseSelectionChanged()
    {
      if( this.SelectionChanged != null )
      {
        this.SelectionChanged( this, EventArgs.Empty );
      }
    }

    private void AssignPathsToRangeDictionaries( 
      DataPath startDatapath, 
      DataPath endDatapath, 
      IEnumerable<SortDescription> sorting, 
      SelectionRange selectionRange )
    {
      DataSourceWrapper dataSource = this.DataGridContext.DataSource;

      foreach( SortDescription sort in sorting )
      {
        if( startDatapath != null )
        {
          object objectValue = dataSource.DataHelper.GetMemberValue( sort.PropertyName, startDatapath );
          selectionRange.StartRangeInfos.Add( sort.PropertyName, objectValue );
        }

        if( endDatapath != null )
        {
          object objectValue = dataSource.DataHelper.GetMemberValue( sort.PropertyName, endDatapath );
          selectionRange.EndRangeInfos.Add( sort.PropertyName, objectValue );
        }
      }
    }

    private void ClearSelectionAnchor()
    {
      m_anchorPath = null;
    }

    private DataPath m_anchorPath;
    private bool m_internalSetSelectedItem;

  }
}
